/*
 * Copyright (c) 2007 Thomas Fors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// =======================================================
// GretagMacbeth Color Checker Routines
// =======================================================


// -------------------------------------------------------
// ColorChecker()
// Constructor
// -------------------------------------------------------
function ColorChecker() {
	
	this.x = new Array();						// array of x-axis pixel coordinates of patches
	this.y = new Array();						// array of y-axiz pixel coordinates of patches
	this.refColor = new Array();				// array of reference patch colors
	this.measColor = new Array();				// array of measured patch colors
	this.bRead = new Array();					// array of boolean flags indicating patch has been read
	
	// Pro Photo RGB Reference
	this.SetReferenceRGB( 0,  81,  66,  52);	// dark skin
	this.SetReferenceRGB( 1, 160, 138, 116);	// light skin
	this.SetReferenceRGB( 2,  94, 102, 134);	// blue sky
	this.SetReferenceRGB( 3,  74,  86,  56);	// foliage
	this.SetReferenceRGB( 4, 118, 111, 154);	// blue flower
	this.SetReferenceRGB( 5, 128, 168, 157);	// bluish green
	this.SetReferenceRGB( 6, 164, 117,  48);	// orange
	this.SetReferenceRGB( 7,  79,  75, 140);	// purplish blue
	this.SetReferenceRGB( 8, 143,  84,  80);	// moderate red
	this.SetReferenceRGB( 9,  68,  51,  83);	// purple
	this.SetReferenceRGB(10, 144, 168,  74);	// yellow green
	this.SetReferenceRGB(11, 184, 155,  61);	// orange yellow
	this.SetReferenceRGB(12,  59,  48, 126);	// blue
	this.SetReferenceRGB(13,  85, 123,  67);	// green
	this.SetReferenceRGB(14, 122,  58,  46);	// red
	this.SetReferenceRGB(15, 200, 188,  68);	// yellow
	this.SetReferenceRGB(16, 142,  83, 123);	// magenta
	this.SetReferenceRGB(17,  76, 108, 145);	// cyan
	this.SetReferenceRGB(18, 241, 241, 241);	// white
	this.SetReferenceRGB(19, 190, 190, 190);	// neutral 8
	this.SetReferenceRGB(20, 145, 145, 145);	// neutral 6.5
	this.SetReferenceRGB(21, 104, 104, 104);	// neutral 5
	this.SetReferenceRGB(22,  67,  67,  67);	// neutral 3.5
	this.SetReferenceRGB(23,  37,  37,  37);	// black
	
	this.MarkAllPatchesUnread();
}

// -------------------------------------------------------
// SetReferenceRGB()
// Sets the reference patch (n) color based on specified
// r, b, g.
// -------------------------------------------------------
ColorChecker.prototype['SetReferenceRGB'] = function (n, r, g, b) {
	this.refColor[n] = new ProPhotoRGBColor();
	this.refColor[n].SetRGB(r, g, b);
}

// -------------------------------------------------------
// MarkAllPatchesUnread()
// -------------------------------------------------------
ColorChecker.prototype['MarkAllPatchesUnread'] = function () {
	var i;
	
	for ( i=0; i<24; i++ ) {
		this.bRead[i] = 0;
	}
}

// -------------------------------------------------------
// FindPatchesFromActiveDoc()
// Reads X,Y coordinates from a four-corner pen path
// in active document and computes color checker patch
// coordinates.
// -------------------------------------------------------
ColorChecker.prototype['FindPatchesFromActiveDoc'] = function () {
	var tlX, tlY, blX, blY, brX, brY, trX, trY;
	
	// Read path to find corner patches
	if ( activeDocument.pathItems.length == 1 ) {	
		// document contains a single path
		if ( activeDocument.pathItems[0].subPathItems[0].pathPoints.length == 4 ) {  
			// path has exactly four points
			tlX = activeDocument.pathItems[0].subPathItems[0].pathPoints[0].anchor[0];
			tlY = activeDocument.pathItems[0].subPathItems[0].pathPoints[0].anchor[1];
			blX = activeDocument.pathItems[0].subPathItems[0].pathPoints[1].anchor[0];
			blY = activeDocument.pathItems[0].subPathItems[0].pathPoints[1].anchor[1];
			brX = activeDocument.pathItems[0].subPathItems[0].pathPoints[2].anchor[0];
			brY = activeDocument.pathItems[0].subPathItems[0].pathPoints[2].anchor[1];
			trX = activeDocument.pathItems[0].subPathItems[0].pathPoints[3].anchor[0];
			trY = activeDocument.pathItems[0].subPathItems[0].pathPoints[3].anchor[1];
		}
		else {
			throw("Path must have exactly four points");
		}
	}
	else {
		throw ("You must define a path");
	}
	
	this.patchCornerString  = "("+Math.round(tlX)+","+Math.round(tlY)+") ";
	this.patchCornerString += "("+Math.round(blX)+","+Math.round(blY)+") ";
	this.patchCornerString += "("+Math.round(brX)+","+Math.round(brY)+") ";
	this.patchCornerString += "("+Math.round(trX)+","+Math.round(trY)+")";
	
	// X,Y coords of corner patches (tl, bl, br, tr)
	this.ComputePatchCoordinates(tlX,tlY, blX,blY, brX,brY, trX,trY);
}

// -------------------------------------------------------
// ComputePatchCoordinates()
// Compute patch locations based on corner X,Y coordinates
// -------------------------------------------------------
ColorChecker.prototype['ComputePatchCoordinates'] = function (tlX,tlY, blX,blY, brX,brY, trX,trY) {
	var row, col;
	
	for ( row = 0; row < 4; row++ ) {
		for ( col = 0; col<6; col++ ) {
			idx = row * 6 + col;  					// patch index
			
			lX = row * (blX - tlX) / 3 + tlX; 		// left x coord of row
			rX = row * (brX - trX) / 3 + trX; 		// right x coord of row
			this.x[idx] = col * (rX - lX) / 5 + lX;	// x coord of patch
			this.x[idx] = Math.round(this.x[idx]);
			
			tY = col * (trY - tlY) / 5 + tlY; 		// top y coord of col
			bY = col * (brY - blY) / 5 + blY; 		// top y coord of col
			this.y[idx] = row * (bY - tY) / 3 + tY;	// y coord of patch
			this.y[idx] = Math.round(this.y[idx]);
		}
	}
	
	// Compute area (K) of quadrilateral abcd with diagonals p,q using Bretschneider's formula
	var a = Math.sqrt(Math.pow(this.x[18]-this.x[ 0], 2) + Math.pow(this.y[18]-this.y[ 0], 2));
	var b = Math.sqrt(Math.pow(this.x[ 0]-this.x[ 5], 2) + Math.pow(this.y[ 0]-this.y[ 5], 2));
	var c = Math.sqrt(Math.pow(this.x[ 5]-this.x[23], 2) + Math.pow(this.y[ 5]-this.y[23], 2));
	var d = Math.sqrt(Math.pow(this.x[23]-this.x[18], 2) + Math.pow(this.y[23]-this.y[18], 2));
	var p = Math.sqrt(Math.pow(this.x[ 0]-this.x[23], 2) + Math.pow(this.y[ 0]-this.y[23], 2));
	var q = Math.sqrt(Math.pow(this.x[18]-this.x[ 5], 2) + Math.pow(this.y[18]-this.y[ 5], 2));
	var K = Math.sqrt(4*p*p*q*q - Math.pow((b*b + d*d - a*a - c*c), 2)) / 4;
	
	// Set max marquee size as fraction of quadrilateral area (K)
	this.maxMarqueeSize = Math.round(Math.sqrt(K/16));
}

// -------------------------------------------------------
// SelectRectangle()
// Select a rectangle defined by its edges
// -------------------------------------------------------
ColorChecker.prototype['SelectRectangle'] = function (t, l, b, r) {
	activeDocument.selection.select(	Array(Array(Math.round(l), Math.round(t)),
											  Array(Math.round(r), Math.round(t)), Array(Math.round(r), Math.round(b)),
											  Array(Math.round(l), Math.round(b)), Array(Math.round(l), Math.round(t))) );
}

// -------------------------------------------------------
// PrevHistState()
// Restore previous history state
// -------------------------------------------------------
ColorChecker.prototype['PrevHistState'] = function () {
    var desc1 = new ActionDescriptor();
	var ref1 = new ActionReference();
	ref1.putEnumerated( charIDToTypeID( "HstS" ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Prvs" ) );
    desc1.putReference( charIDToTypeID( "null" ), ref1 );
	
	executeAction( charIDToTypeID( "slct" ), desc1, DialogModes.NO );
}

// -------------------------------------------------------
// GetChannelHist()
// Get histogram for a particular channel: "Rd  ", "Grn ", or "Bl  "
// -------------------------------------------------------
ColorChecker.prototype['GetChannelHist'] = function (chan) {
    var desc1 = new ActionDescriptor();
    desc1.putBoolean( charIDToTypeID( "Mnch" ), true );
	var desc2 = new ActionDescriptor();
	desc2.putUnitDouble( charIDToTypeID( chan ), charIDToTypeID( "#Prc" ), 100.000000 );
    desc1.putObject( charIDToTypeID( "Gry " ), charIDToTypeID( "ChMx" ), desc2 );
	
	executeAction( charIDToTypeID( "ChnM" ), desc1, DialogModes.NO );
	
    var rv = activeDocument.histogram;
	
    this.PrevHistState();
	
    return rv;
}

// -------------------------------------------------------
// ReadPatch()
// Read a single ColorChecker patch
// -------------------------------------------------------
ColorChecker.prototype['ReadPatch'] = function (idx) {
	var color;
	var nTries = 0;
	var bSuccess = false;
	
	while ( !bSuccess && nTries < 3 ) {
		try {
			if ( this.bRead[idx] != 0 ) {
				color = this.measColor[idx];
			} else {
				var i;
				color = new SolidColor();
				var size = this.maxMarqueeSize/12;
				this.SelectRectangle(this.y[idx]-size, this.x[idx]-size, this.y[idx]+size, this.x[idx]+size);
				
				// Get data
				var histRed = this.GetChannelHist("Rd  ");
				var histGrn = this.GetChannelHist("Grn ");
				var histBlu = this.GetChannelHist("Bl  ");

				this.PrevHistState();
				
				// Get total pixel count
				var totRed = 0;
				var totGrn = 0;
				var totBlu = 0;
				for ( i=0; i<256; i++ ) {
					totRed += histRed[i];
					totGrn += histGrn[i];
					totBlu += histBlu[i];
				}
				
				// Compute r,g,b
				var red = 0;
				var grn = 0;
				var blu= 0;
				for ( i=0; i<256; i++ ) {
					red += i * histRed[i] / totRed;
					grn += i * histGrn[i] / totGrn;
					blu += i * histBlu[i] / totBlu;
				}
				
				// return color
				color = new ProPhotoRGBColor();
				color.SetRGB(red, grn, blu);
				
				// indicate that patch was read for future lookups
				this.measColor[idx] = color;
				this.bRead[idx] = 1;
			}
			
			bSuccess = true;
		} catch (e) {
			// raw file was not active window.  Select it and try again.
			nTries++;
			display.SelectRawWnd();
		}
	}
	
	return color;
}

// -------------------------------------------------------
// GetWbError()
// Returns white balance error of patch 19
// -------------------------------------------------------
ColorChecker.prototype['GetWbError'] = function () {
	acr.Open();
	var color = this.ReadPatch(19);
	var err = Math.sqrt((color.a*color.a + color.b*color.b));
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetGrayError()
// Returns RMS luminance error of gray patches
// -------------------------------------------------------
ColorChecker.prototype['GetGrayError'] = function () {
	acr.Open();
	var err = 0;
	err += 1 * Math.pow( (cc.refColor[19].L - cc.ReadPatch(19).L), 2 );
	err += 1 * Math.pow( (cc.refColor[20].L - cc.ReadPatch(20).L), 2 );
	err += 1 * Math.pow( (cc.refColor[21].L - cc.ReadPatch(21).L), 2 );
	err += 1 * Math.pow( (cc.refColor[22].L - cc.ReadPatch(22).L), 2 );
	err += 1 * Math.pow( (cc.refColor[23].L - cc.ReadPatch(23).L), 2 );
	err /= 5;
	err = Math.sqrt(err); // RMS
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetSatError()
// Returns saturation error of green patch
// -------------------------------------------------------
ColorChecker.prototype['GetSatError'] = function (settings) {
	var err = 0;
	
	acr.Open();
	err += Math.pow( (cc.refColor[13].G - cc.ReadPatch(13).G), 2 );
	err = Math.sqrt(err);
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetShadowTintError()
// Returns white balance error of patch 23
// -------------------------------------------------------
ColorChecker.prototype['GetShadowTintError'] = function () {
	acr.Open();
	var color = this.ReadPatch(23);
	var err = Math.sqrt((color.a*color.a + color.b*color.b));
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetGreenError()
// Returns error of green patch
// -------------------------------------------------------
ColorChecker.prototype['GetGreenError'] = function () {
	acr.Open();
	var err = Math.sqrt( Math.pow( (cc.refColor[13].a - cc.ReadPatch(13).a), 2 ) + Math.pow( (cc.refColor[13].b - cc.ReadPatch(13).b), 2 ) );
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetBlueError()
// Returns error of blue patch
// -------------------------------------------------------
ColorChecker.prototype['GetBlueError'] = function () {
	acr.Open();
	var err = Math.sqrt( Math.pow( (cc.refColor[12].a - cc.ReadPatch(12).a), 2 ) + Math.pow( (cc.refColor[12].b - cc.ReadPatch(12).b), 2 ) );
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// GetRedError()
// Returns error of red patch
// -------------------------------------------------------
ColorChecker.prototype['GetRedError'] = function () {
	acr.Open();
	var err = Math.sqrt( Math.pow( (cc.refColor[14].a - cc.ReadPatch(14).a), 2 ) + Math.pow( (cc.refColor[14].b - cc.ReadPatch(14).b), 2 ) );
	acr.Close();
	
	return err;
}

// -------------------------------------------------------
// WhiteBalance()
// Performs a white balance
// -------------------------------------------------------
ColorChecker.prototype['WhiteBalance'] = function (n) {
	var settings;
	var optimizer;
	
	var msg = "White Balance";
	if ( n == 2 )
		msg = "White Balance (2nd pass)";
	
	optimizer = new Simplex(2, msg);
	optimizer.SetDimension(0, 2000, 50000, 50, "SetColorTemp");
	optimizer.SetDimension(1, -150, 150, 1, "SetTint");
	optimizer.DefineOptimization(-1, "GetWbError", 0.1, undefined, undefined, 0);
	settings = new Array(2);
	settings[0] = acr.temp;
	settings[1] = acr.tint;
	optimizer.Optimize(settings);	// initial settings
	display.temp = acr.temp;
	display.tint = acr.tint;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// GrayAdjust()
// Optimizes gray patches
// -------------------------------------------------------
ColorChecker.prototype['GrayAdjust'] = function () {
	var settings;
	var optimizer;
	
	optimizer = new Simplex(4, "Gray Patches");
	optimizer.SetDimension(0, -4, 4, .05, "SetExposure");
	optimizer.SetDimension(1, 0, 100, 1, "SetShadows");
	optimizer.SetDimension(2, 0, 150, 1, "SetBrightness");
	optimizer.SetDimension(3, -50, 100, 1, "SetContrast");
	optimizer.DefineOptimization(-1, "GetGrayError", 0, undefined, undefined, 0);
	settings = new Array(4);
	settings[0] = acr.exps;
	settings[1] = acr.shad;
	settings[2] = acr.brgh;
	settings[3] = acr.cntr;
	optimizer.Optimize(settings);	// initial settings
	display.exps = acr.exps;
	display.shad = acr.shad;
	display.brgh = acr.brgh;
	display.cntr = acr.cntr;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// SaturationAdjust()
// Optimizes saturation control for green component of
// green patch
// -------------------------------------------------------
ColorChecker.prototype['SaturationAdjust'] = function () {
	//
	// Disabled since it appears to cause more damage than good
	//
	
	//var settings;
	//var optimizer;
	
	//optimizer = new Simplex(1, "Working on saturation...");
	//optimizer.SetDimension(0, -100, 100, 1, "SetSaturation");
	//optimizer.DefineOptimization(-1, "GetSatError", 0.01, 10, undefined, 1);
	//settings = new Array(1);
	//settings[0] = acr.strt;
	//optimizer.Optimize(settings);	// initial settings
	display.strt = acr.strt;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// ShadowTintAdjust()
// Optimizes shadow tint control for neutrality in
// black patch
// -------------------------------------------------------
ColorChecker.prototype['ShadowTintAdjust'] = function () {
	var settings;
	var optimizer;
	
	optimizer = new Simplex(1, "Shadow Tint");
	optimizer.SetDimension(0, -100, 100, 1, "SetShadowTint");
	optimizer.DefineOptimization(-1, "GetShadowTintError", 0, undefined, undefined, 0);
	settings = new Array(1);
	settings[0] = acr.blkb;
	optimizer.Optimize(settings);	// initial settings
	display.blkb = acr.blkb;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// GreenCalibration()
// Adjusts the green controls on the ACR calibration tab
// -------------------------------------------------------
ColorChecker.prototype['GreenCalibration'] = function (n) {
	var settings;
	var optimizer;
	
	var msg = "Green Calibration";
	if ( n == 2 )
		msg = "Green Calibration (2nd pass)";
	if ( n == 3 )
		msg = "Green Calibration (3rd pass)";
	
	optimizer = new Simplex(2, msg);
	optimizer.SetDimension(0, -100, 100, 1, "SetGreenHue");
	optimizer.SetDimension(1, -100, 100, 1, "SetGreenSat");
	optimizer.DefineOptimization(-1, "GetGreenError", 0, undefined, undefined, 0);
	settings = new Array(2);
	settings[0] = acr.ghue;
	settings[1] = acr.gsat;
	optimizer.Optimize(settings);	// initial settings
	display.ghue = acr.ghue;
	display.gsat = acr.gsat;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// BlueCalibration()
// Adjusts the blue controls on the ACR calibration tab
// -------------------------------------------------------
ColorChecker.prototype['BlueCalibration'] = function (n) {
	var settings;
	var optimizer;
	
	var msg = "Blue Calibration";
	if ( n == 2 )
		msg = "Blue Calibration (2nd pass)";
	if ( n == 3 )
		msg = "Blue Calibration (3rd pass)";
	
	optimizer = new Simplex(2, msg);
	optimizer.SetDimension(0, -100, 100, 1, "SetBlueHue");
	optimizer.SetDimension(1, -100, 100, 1, "SetBlueSat");
	optimizer.DefineOptimization(-1, "GetBlueError", 0, undefined, undefined, 0);
	settings = new Array(2);
	settings[0] = acr.bhue;
	settings[1] = acr.bsat;
	optimizer.Optimize(settings);	// initial settings
	display.bhue = acr.bhue;
	display.bsat = acr.bsat;
	display.DisplayStatus(" ");
}

// -------------------------------------------------------
// RedCalibration()
// Adjusts the red controls on the ACR calibration tab
// -------------------------------------------------------
ColorChecker.prototype['RedCalibration'] = function (n) {
	var settings;
	var optimizer;
	
	var msg = "Red Calibration";
	if ( n == 2 )
		msg = "Red Calibration (2nd pass)";
	if ( n == 3 )
		msg = "Red Calibration (3rd pass)";
	
	optimizer = new Simplex(2, msg);
	optimizer.SetDimension(0, -100, 100, 1, "SetRedHue");
	optimizer.SetDimension(1, -100, 100, 1, "SetRedSat");
	optimizer.DefineOptimization(-1, "GetRedError", 0, undefined, undefined, 0);
	settings = new Array(2);
	settings[0] = acr.rhue;
	settings[1] = acr.rsat;
	optimizer.Optimize(settings);	// initial settings
	display.rhue = acr.rhue;
	display.rsat = acr.rsat;
	display.DisplayStatus(" ");
}
